3-5 CLI初试:启动 Nest 应用程序 + 创建控制器
启动Nest应用程序
开发服务器启动
使用 npm run start:dev
命令启动开发服务器:
npm run start:dev
bash
- 热重载机制:修改代码后自动重启服务,无需手动停止和重启。
💡 底层基于nodemon
实现,支持自定义监控文件扩展名(通过nodemon.json
配置)。 - 默认访问地址:
http://localhost:3000
💡 端口可通过main.ts
中的app.listen(port)
修改,例如:await app.listen(4000); // 改为4000端口
typescript - 监控控制台日志:
- 启动成功日志:
Nest application successfully started
- 错误日志:如端口冲突会提示
EADDRINUSE
- 请求日志:默认显示HTTP方法和路径(需开启日志中间件)。
- 启动成功日志:
常见问题解答 ❓
- 启动失败怎么办?
- 检查
node_modules
是否完整(尝试npm install
)。 - 确认端口未被占用(如
3000
被占用,可修改端口)。
- 检查
- 如何自定义热重载行为?
在项目根目录创建nodemon.json
:{ "watch": ["src"], "ext": "ts,json", "ignore": ["src/**/*.spec.ts"] }
json
基础项目结构
核心文件说明:
main.ts
- 作用:应用入口,初始化Nest实例并启动HTTP服务。
- 关键代码:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();
typescript
app.module.ts
- 作用:根模块,定义应用的依赖关系和模块组织。
- 关键代码:
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ controllers: [AppController], providers: [AppService], }) export class AppModule {}
typescript
app.controller.ts
- 作用:处理HTTP请求,定义路由和响应逻辑。
- 示例:
import { Controller, Get } from '@nestjs/common'; @Controller() export class AppController { @Get() getHello(): string { return 'Hello World!'; } }
typescript
app.service.ts
- 作用:封装业务逻辑,供控制器调用。
- 示例:
import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello(): string { return 'Hello from Service!'; } }
typescript
扩展知识 🔍
- 模块化设计:NestJS采用模块化架构,推荐按功能划分模块(如
UserModule
、AuthModule
)。 - 依赖注入:通过
@Injectable()
装饰器标记服务类,由框架自动管理实例生命周期。
实践建议 🛠️
- 调试技巧:
- 使用
console.log
或Logger
服务输出调试信息。 - 结合VS Code的调试配置(
launch.json
)设置断点。
- 使用
- 性能优化:
- 启用压缩中间件:
import * as compression from 'compression'; app.use(compression());
typescript
- 启用压缩中间件:
通过以上扩展,你可以更全面地掌握Nest应用的启动流程和项目结构设计! 🚀
控制器开发
创建基础路由
在控制器文件中添加GET路由:
import { Controller, Get } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
getHello(): string {
return 'Hello NestJS!';
}
}
typescript
核心知识点:
@Controller()
:类装饰器,标记该类为控制器@Get()
:方法装饰器,定义HTTP GET路由- 返回值处理:直接返回字符串时,Nest会自动设置
Content-Type: text/html
扩展功能:
- 路径参数:
@Get(':id')
getById(@Param('id') id: string) {
return `Item ${id}`;
}
typescript
- 异步处理:
@Get()
async getData() {
const data = await fetchData(); // 模拟异步操作
return data;
}
typescript
💡 最佳实践:
- 保持控制器方法简洁,复杂逻辑应放在Service层
- 使用DTO(Data Transfer Object)定义请求/响应数据结构
路由前缀设置
通过控制器装饰器参数添加路径前缀:
@Controller('api')
export class ApiController {
@Get('hello')
getHello() {
return { message: 'Hello with prefix' };
}
}
typescript
高级用法:
- 多级前缀:
@Controller('v1/api') // 访问路径:/v1/api/hello
typescript
- 动态前缀(基于环境变量):
@Controller(process.env.API_PREFIX || 'api')
typescript
- 路由通配符:
@Get('ab*cd') // 匹配abcd、ab_cd等路径
typescript
常见问题:
- 路径冲突:避免在不同控制器中定义相同路径
- 版本控制:推荐使用路径前缀进行API版本管理(如
/v1/
、/v2/
)
自动JSON序列化
NestJS自动转换返回值为JSON格式:
@Get('user')
getUser() {
return {
code: 0,
data: {
id: 1,
name: 'Alice',
createdAt: new Date() // 日期对象也会被自动序列化
},
message: 'Success'
};
}
typescript
技术细节:
- 序列化机制:
- 基于
class-transformer
库 - 支持嵌套对象、数组等复杂结构
- 自动处理循环引用
- 基于
- 自定义序列化:
import { Expose } from 'class-transformer';
class UserDto {
@Expose()
id: number;
@Expose({ name: 'fullName' })
name: string;
}
typescript
- 性能优化:
- 大量数据时建议使用流式响应
- 敏感字段可使用
@Exclude()
排除
错误处理:
@Get('error')
getError() {
throw new HttpException('Not Found', HttpStatus.NOT_FOUND);
// 自动返回:{ "statusCode": 404, "message": "Not Found" }
}
typescript
实战案例:用户控制器
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll() {
const users = await this.usersService.findAll();
return {
code: 200,
data: users,
timestamp: new Date().toISOString()
};
}
@Post()
@HttpCode(201)
async create(@Body() createUserDto: CreateUserDto) {
const user = await this.usersService.create(createUserDto);
return user;
}
}
typescript
扩展学习资源
通过以上扩展内容,你可以全面掌握NestJS控制器的各种高级用法和最佳实践! 🚀
CLI代码生成
模块创建命令
nest g module users
bash
核心功能
- 文件生成:自动创建
src/users/users.module.ts
- 模块注册:自动将新模块添加到最近的父模块(通常是
AppModule
) - 代码结构:
import { Module } from '@nestjs/common'; @Module({}) export class UsersModule {}
typescript
高级用法
- 路径自定义:
nest g module features/users --flat
bash--flat
:不创建子目录- 生成路径:
src/features/users.module.ts
- 模块类型:
nest g module users --type=shared
bash- 创建共享模块(带
@Global()
装饰器)
- 创建共享模块(带
- 即时更新:
- 修改模块后自动触发热重载
- 支持
--dry-run
参数预览变更
💡 最佳实践:
- 按业务功能划分模块(如
AuthModule
、OrderModule
) - 使用
--no-spec
跳过测试文件生成(nest g module users --no-spec
)
控制器创建命令
nest g controller users --no-spec
bash
生成内容
- 基础结构:
import { Controller } from '@nestjs/common'; @Controller('users') export class UsersController {}
typescript - 自动注册:
- 自动添加到所属模块的
controllers
数组 - 支持 RESTful 标准方法装饰器(
@Get()
、@Post()
等)
- 自动添加到所属模块的
定制选项
参数 | 作用 | 示例 |
---|---|---|
--flat | 不创建子目录 | nest g controller users --flat |
--path | 自定义路由前缀 | nest g controller users --path=api |
--spec | 强制生成测试文件 | nest g controller users --spec |
常见问题
- 路径冲突:多个控制器使用相同路由前缀时需注意
- 依赖注入:通过构造函数自动注入服务
constructor(private readonly usersService: UsersService) {}
typescript
服务层创建命令
nest g service users --no-spec
bash
生成文件解析
- 服务类:
import { Injectable } from '@nestjs/common'; @Injectable() export class UsersService {}
typescript - 自动注入:
- 自动注册到模块的
providers
数组 - 支持通过
@Inject()
自定义注入令牌
- 自动注册到模块的
扩展功能
- 自定义装饰器:
@Injectable() @LogExecutionTime() // 自定义装饰器 export class UsersService {}
typescript - 生命周期钩子:
@Injectable() export class UsersService implements OnModuleInit { onModuleInit() { console.log('Service initialized'); } }
typescript - 多服务生成:
nest g service users/account --no-spec
bash- 创建嵌套服务:
src/users/services/account.service.ts
- 创建嵌套服务:
性能优化
- 作用域控制:
@Injectable({ scope: Scope.REQUEST }) // 请求级实例 export class UsersService {}
typescript
综合工作流示例
典型用例
- 完整生成:
nest g resource users --no-spec
bash- 一键生成模块、控制器、服务、DTO等
- 自定义模板:
- 修改
nest-cli.json
使用自定义模板:
{ "generateOptions": { "spec": false, "template": "./my-template" } }
json - 修改
调试技巧
- 查看生成代码:使用
--dry-run
预览 - 回滚操作:手动删除生成的文件和模块引用
通过掌握这些CLI技巧,你的开发效率将显著提升! 🚀
分层架构实现
Controller-Service协作
核心实现
// users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll() {
return await this.usersService.getAllUsers();
}
}
typescript
// users.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
async getAllUsers() {
// 模拟数据库查询
return [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
}
}
typescript
进阶特性
- DTO数据传输对象
// create-user.dto.ts
export class CreateUserDto {
readonly name: string;
readonly email: string;
}
// 在控制器中使用
@Post()
async create(@Body() createUserDto: CreateUserDto) {
return this.usersService.createUser(createUserDto);
}
typescript
- 异常处理
// users.service.ts
async getUserById(id: number) {
const user = await this.findUser(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
typescript
- 日志记录
import { Logger } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
async getAllUsers() {
this.logger.log('Fetching all users');
// ...
}
}
typescript
依赖注入机制
全局路由前缀配置
基础配置
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 设置全局前缀
app.setGlobalPrefix('api/v1', {
exclude: ['health'] // 排除特定路由
});
await app.listen(3000);
}
typescript
高级用法
- 动态前缀配置
const prefix = process.env.API_PREFIX || 'api/v1';
app.setGlobalPrefix(prefix);
typescript
- 多版本控制
// 通过中间件实现版本路由
app.use((req, res, next) => {
if (req.url.startsWith('/v1/')) {
req.url = req.url.replace('/v1', '');
}
next();
});
typescript
- Swagger文档集成
const options = new DocumentBuilder()
.setTitle('API文档')
.setVersion('1.0')
.addServer('http://localhost:3000/api/v1')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('docs', app, document);
typescript
配置建议
配置项 | 说明 | 示例值 |
---|---|---|
前缀路径 | 基础API路径 | api/v1 |
排除路由 | 不需要前缀的路由 | ['health', 'metrics'] |
环境变量 | 动态配置前缀 | process.env.API_PREFIX |
最佳实践
- 分层规范
- Controller:只处理HTTP请求和响应
- Service:实现业务逻辑
- Repository:处理数据持久化
- 版本控制策略
- URI路径版本控制(
/v1/users
) - 请求头版本控制(
Accept: application/vnd.myapi.v1+json
)
- URI路径版本控制(
- 性能优化
// 启用缓存 @Get() @CacheTTL(60) // 60秒缓存 async findAll() { return this.usersService.getAllUsers(); }
typescript
常见问题解决方案
- 循环依赖问题
- 使用
forwardRef()
解决模块间循环依赖
@Module({ imports: [forwardRef(() => AModule)] }) export class BModule {}
typescript - 使用
- 路由冲突处理
- 明确路径优先级
@Get('special') getSpecial() {} @Get(':id') getById() {}
typescript - 依赖注入错误
- 确保服务类有
@Injectable()
装饰器 - 检查模块的
providers
数组是否包含该服务
- 确保服务类有
通过这种分层架构,你的NestJS应用将获得:
- 更好的可维护性 🛠️
- 更清晰的职责划分 📊
- 更强的扩展能力 🚀
- 更便捷的团队协作 👥
记住:良好的架构是项目成功的基础!建议结合Swagger文档和单元测试来确保代码质量。
接口测试工具
Postman环境配置
1. 环境创建与管理
步骤详解:
- 点击右上角环境切换按钮 → "Manage Environments"
- 新建环境命名为
dev
(开发环境)和prod
(生产环境) - 添加关键变量:
{ "base_url": "http://localhost:3000", "api_key": "your_dev_key", "auth_token": "{{login_response.body.token}}" }
json
高级技巧:
- 动态变量:使用
{{}}
引用其他请求的响应值"user_id": "{{create_user_response.body.id}}"
json - 环境继承:通过
initial value
和current value
区分默认值和覆盖值 - 共享配置:导出环境文件给团队成员使用
2. 请求模板配置
// Pre-request Script示例(自动添加headers)
pm.environment.set("request_timestamp", new Date());
pm.request.headers.add({
key: "X-Request-ID",
value: pm.variables.replaceIn("{{$guid}}")
});
javascript
3. 自动化测试脚本
// Tests脚本示例(响应断言)
pm.test("Status code is 200", () => pm.response.to.have.status(200));
pm.test("Response time < 200ms", () => pm.expect(pm.response.responseTime).to.be.below(200));
pm.test("Has valid JSON body", () => pm.response.to.have.jsonBody());
javascript
请求测试示例
完整测试用例集
方法 | 路径 | 请求体示例 | 预期状态码 | 响应断言 |
---|---|---|---|---|
GET | /api/v1/users | - | 200 | pm.expect(jsonData).to.be.an('array').with.lengthOf(2) |
POST | /api/v1/users | { "name": "Charlie" } | 201 | pm.expect(jsonData).to.have.property('id').that.is.a('number') |
GET | /api/v1/users/range | ?number=3 | 200 | pm.expect(jsonData).to.eql(["1","2","3"]) |
PUT | /api/v1/users/1 | { "name": "Alice_updated" } | 200 | pm.expect(jsonData.name).to.include('updated') |
DELETE | /api/v1/users/1 | - | 204 | pm.expect(pm.response.code).to.be.oneOf([200, 204]) |
异常测试场景
测试案例 | 预期结果 |
---|---|
POST空数据 | 400状态码 + { "error": "Validation failed" } |
GET不存在的ID (/users/99 ) | 404状态码 + { "message": "User not found" } |
无权限访问(未传token) | 401状态码 + { "statusCode": 401, "message": "Unauthorized" } |
自动化测试进阶
1. 测试集合运行
2. CI/CD集成
# GitHub Actions示例
- name: Run API Tests
uses: postmanlabs/newman-action@v1
with:
collection: ./tests/api_collection.json
environment: ./tests/dev_environment.json
reporters: cli,json
yaml
3. 性能测试
// 在Postman中设置循环次数
for (let i = 0; i < 10; i++) {
pm.sendRequest({
url: pm.variables.get("base_url") + "/stress-test",
method: "GET"
}, (err, res) => {
console.log(res.json());
});
}
javascript
替代工具推荐
工具名称 | 特点 | 适用场景 |
---|---|---|
Insomnia | 开源/轻量级/支持GraphQL | 开发调试阶段 |
Swagger UI | 自动生成文档/直接测试 | API文档共享 |
JMeter | 专业性能测试/分布式压测 | 高并发场景测试 |
最佳实践
- 版本控制:将Postman集合导出为JSON纳入Git管理
- 数据隔离:为每个测试用例使用独立测试数据
- 监控告警:配置Newman测试失败自动通知(如Slack/webhook)
- 测试覆盖率:确保覆盖:
- 所有HTTP方法(GET/POST/PUT/DELETE)
- 边界值测试(空值/超长字符串)
- 安全测试(XSS/SQL注入尝试)
通过完善的接口测试方案,你可以确保API的:
✅ 功能正确性
✅ 性能稳定性
✅ 安全可靠性
✅ 变更兼容性
建议每周运行完整的回归测试套件,持续保障接口质量!
实践作业:range接口
任务要求详解
基础功能实现
import { Controller, Get, Query, BadRequestException } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get('range')
getRange(@Query('number') numberStr: string): string[] {
// 参数转换与验证
const num = parseInt(numberStr, 10);
if (isNaN(num)) {
throw new BadRequestException('Parameter must be a valid number');
}
// 生成数字序列
return Array.from({ length: num }, (_, i) => (i + 1).toString());
}
}
typescript
关键点解析
@Query()
装饰器:自动解析URL查询参数- 类型转换:
parseInt
的第二个参数10确保十进制解析 - 异常处理:
BadRequestException
符合RESTful规范 - 数组生成:
Array.from
比for循环
更简洁
进阶实现方案
1. 增强参数验证
import { IsNumberString, Min } from 'class-validator';
class RangeQueryDto {
@IsNumberString()
@Min(1, { message: 'Number must be positive' })
number: string;
}
@Get('range')
getRange(@Query() query: RangeQueryDto) {
const num = parseInt(query.number, 10);
return Array.from({ length: num }, (_, i) => (i + 1).toString());
}
typescript
💡 使用class-validator
实现声明式验证
2. Swagger文档集成
import { ApiQuery, ApiResponse } from '@nestjs/swagger';
@ApiQuery({
name: 'number',
type: Number,
description: '生成序列的长度',
example: 5,
required: true
})
@ApiResponse({
status: 200,
description: '生成的数字序列',
type: [String],
example: ["1","2","3","4","5"]
})
@ApiResponse({
status: 400,
description: '参数无效错误'
})
@Get('range')
// ...方法实现
typescript
3. 性能优化版
const rangeCache = new Map<number, string[]>();
@Get('range')
getRange(@Query('number') numberStr: string) {
const num = parseInt(numberStr, 10);
if (num <= 0) throw new BadRequestException('Number must be positive');
// 缓存机制
if (!rangeCache.has(num)) {
rangeCache.set(num, Array.from({ length: num }, (_, i) => (i + 1).toString()));
}
return rangeCache.get(num);
}
typescript
测试用例设计
1. 单元测试(Jest)
describe('UsersController', () => {
let controller: UsersController;
beforeEach(() => {
controller = new UsersController();
});
test('should return correct range', () => {
expect(controller.getRange('5')).toEqual(["1","2","3","4","5"]);
});
test('should throw on invalid input', () => {
expect(() => controller.getRange('abc')).toThrow(BadRequestException);
});
});
typescript
2. Postman测试集
{
"item": [
{
"name": "Valid Request",
"request": {
"method": "GET",
"url": "{{base_url}}/users/range?number=3"
},
"tests": "pm.expect(pm.response.json()).to.eql(['1','2','3'])"
},
{
"name": "Negative Number",
"request": {
"method": "GET",
"url": "{{base_url}}/users/range?number=-1"
},
"tests": "pm.expect(pm.response.code).to.equal(400)"
}
]
}
json
常见问题解决方案
- 浮点数处理:
if (!Number.isInteger(num)) { throw new BadRequestException('Number must be an integer'); }
typescript - 超大数字防护:
if (num > 1000) { throw new BadRequestException('Number cannot exceed 1000'); }
typescript - 多语言错误消息:
throw new BadRequestException({ errorCode: 'INVALID_NUMBER', message: { en: 'Invalid number parameter', zh: '参数必须是有效数字' } });
typescript
扩展思考题
- 如何实现字母序列生成?(A-Z, AA-AZ等)
- 如果要求返回JSON格式
{ "result": ["1","2","3"] }
该如何修改? - 如何让接口支持
?start=5&end=10
的参数形式?
提交模板建议
# 作业提交:range接口实现
**文件位置**:`src/users/users.controller.ts`
## 核心代码
```typescript
// 你的完整控制器代码
markdown
实现亮点
- 使用了class-validator进行参数校验
- 添加了Swagger文档注解
- 实现了缓存优化
测试结果
测试案例 | 结果 |
---|---|
正常输入 | ✅ |
非数字输入 | ✅ |
负数输入 | ✅ |
通过这个实践作业,你将掌握:
- 查询参数处理 🎯
- 数据验证技巧 🔍
- 接口文档化 📄
- 性能优化思维 ⚡
记得在测试时覆盖边界值情况!
text
↑